// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © BruzX
// ╔───────────────────────────────────────────────────────────────────────────────────╗
// │                                                                                   │
// │ __/\\\\\\\\\\\\\______/\\\\\\\\\______/\\\________/\\\__/\\\\\\\\\\\\\\\_         │
// │  _\/\\\/////////\\\__/\\\///////\\\___\/\\\_______\/\\\_\////////////\\\__        │
// │   _\/\\\_______\/\\\_\/\\\_____\/\\\___\/\\\_______\/\\\___________/\\\/___       │
// │    _\/\\\\\\\\\\\\\\__\/\\\\\\\\\\\/____\/\\\_______\/\\\_________/\\\/_____      │
// │     _\/\\\/////////\\\_\/\\\//////\\\____\/\\\_______\/\\\_______/\\\/_______     │
// │      _\/\\\_______\/\\\_\/\\\____\//\\\___\/\\\_______\/\\\_____/\\\/_________    │
// │       _\/\\\_______\/\\\_\/\\\_____\//\\\__\//\\\______/\\\____/\\\/___________   │
// │        _\/\\\\\\\\\\\\\/__\/\\\______\//\\\__\///\\\\\\\\\/____/\\\\\\\\\\\\\\\_  │
// │         _\/////////////____\///________\///_____\/////////_____\///////////////__ │
// │                                                                                   │
// ╚───────────────────────────────────────────────────────────────────────────────────╝

//TODO: Free version: remove plays, leave candidate plays, option presets, alerts, monthly/weekly pivots, remove futures options, remvoe configurable targets

//@version=6
indicator("Camarilla Pivot Plays (Lite) [BruzX]", shorttitle = "CPP(L)", overlay = true, max_boxes_count = 100, max_lines_count = 100)

// DEVELOPER MODE
const bool DEV_MODE = false

//#region ---- Enums

//@enum             Two day Camarilla range relationshop
//@field higher     Yesterday's range is higher than 2 day's ago
//@field lower      Yesterday's range is lower than 2 day's ago
//@field neutral    Yesterday's and 2 day's ago ranges are the same within a configurable tolerance
enum PivotRange
    higher = "Higher Range"
    lower = "Lower Range"
    neutral = "Neutral Range"

//@enum             Used for both current pivot width and Which pivot width is a play valid for
//@field wide       Currently wide pivots. Also play is valid with wide pivots
//@field narrow     Currently narrow pivots. Also play is valid with narrow pivots
//@field similar    Currently similar pivots
//@field versatile  Play is valid with both wide and narrow pivots
enum PivotWidth
    wide = "Wide Pivots"
    narrow = "Narrow Pivots"
    similar = "Similar Pivots"
    versatile = "Versatile"

//@enum             Controls how the Cams are calculate
//@field auto       Automically decides whether to use ETH or not depending on whether ETH range is witint RTH range
//@field forceRTH   Only use RTH close, high, low to calculate Cams
//@field forceETH   Always use ETH close, high, low to calculate Cams
enum CalcMode
    auto = "Auto"
    forceRTH = "Force RTH Data"
    forceETH = "Force ETH Data"

//@enum             Play direction
//@field long       Play with a long entry
//@field short      Play with a short entry
enum Direction
    long
    short

//@enum         Option for forcing a particular close in the calclations
//@field auto   Use the same close as decided by the calculation mode
//@field rth    Force RTH close in all cases
//@field eth    Force ETH close in all cases
enum UseClose
    auto = "Auto"
    rth = "Force RTH close"
    eth = "Force ETH close"

//#endregion ---- Enums

//#region ---- UDTs

//@type     Holds play state variables between bars. Just using vars in the script logic would have caused a huge number of variables across the 12 plays
type PlayState
    bool playPossible    
    bool play_blocked
    
//@type     Holds the play specifications
type PlaySpec
    string description
    Direction direction
    PivotRange validRange
    PivotWidth validWidth
    bool entry_precondition_region   
    bool entry_precondition_rthOpen    
    bool block_play
    
//@type             Container for handles to a line and a label
//@field h_line     Line handle
//@field h_label    Line handle
type ExtLine
    line h_line
    label h_label

//#endregion ---- UDTs

//#region ---- Constants

// Specfies levels and plays
const array<string> CAM_LEVELS = array.from("R3", "R4", "R6", "S3", "S4", "S6")
const array<string> PLAYS = array.from("HA", "HB", "HC", "HD", "HE", "HF", "LA", "LB", "LC", "LD", "LE", "LF")

// Contains play specifications
const map<string, PlaySpec> playSpecMap = map.new<string,PlaySpec>()

// Contains state of plays we need to keep over time (except play activation status, see below)
var const map<string, PlayState> playStateMap = map.new<string, PlayState>()

//#endregion ---- Constants

//#region ---- Inputs 

string g_optionsPresets = "Options presets"
string g_displayOptions = "Display options"
string g_behaviourOptions = "Behaviour options"
string g_playPreconditions = "Play preconditions"
string g_playTargets = "Play targets"
string g_futuresOptions = "Futures options"
string g_developerOptions = "Developer options"

bool userConf = input.bool(false, "I confirm I've done what's written in the tooltip. See the 🛈 icon ➜", tooltip = "Tick this checkbox to confirm you have read the indicator description on the TradingView website, understand the plays and what the play codes mean. Until this is done, the indicator won't display or alert any plays")

// Options affecting display
bool lightMode = input.bool(false, "Light background mode", tooltip = "Tick if your charts have a light coloured background. By default the label text colour is chosen for best contrast against its background taking play box transparency into account and assumes a dark chart canvas. This option selects the best text colour if the chart canvas is light", group = g_displayOptions)
int numDays = input.int(1, "Number of prior days", 0, 20, tooltip = "Number of days to show prior to current day. This setting is not guaranteed to be exact", display = display.all - display.status_line, group = g_displayOptions)
bool showCP = input.bool(true, "Show Central Pivot (CP)", tooltip = "Show the Central Pivot, which is either RTH close or ETH close from the previous day, depending on which data was in use on that day", group = g_displayOptions)
bool showCPR = input.bool(false, "Show Central Pivot Range (CPR)", tooltip = "Show the Central Pivot Range, which is the range between R2 and S2", group = g_displayOptions)
bool showPrices = input.bool(false, "Show prices on labels", tooltip = "Show the price of the pivot lines on their labels", group = g_displayOptions)
bool lineJoin = input.bool(false, "Join lines", tooltip = "Connects the pivot lines over session boundaries", group = g_displayOptions)
bool extendLines = input.bool(false, "Extend Lines", tooltip = "Extend the pivot lines right for the current day", group = g_displayOptions)
int labelOffset = input.int(10, "Label offset", tooltip = "Horizontal label offset, prevents overlaps with other indicator labels", display = display.all - display.status_line, group = g_displayOptions)
color supportColour = input.color(color.green, "Support col.", inline = "colours", group = g_displayOptions)
color resistanceColour = input.color(color.red, "Resistance col.", inline = "colours", group = g_displayOptions)
color cpColour = input.color(color.blue, "CP col.", inline = "colours", group = g_displayOptions)
color cprColour = input.color(color.new(color.yellow, 90), "CPR col.", inline = "colours", group = g_displayOptions)

// Options affecting behaviour
CalcMode calcMode = input.enum(CalcMode.auto, "Calculation mode", tooltip = "Controls whether to always or never use ETH data or whether to handle automatically", group = g_behaviourOptions)
UseClose useClose = input.enum(UseClose.auto, "Use close", tooltip = "Which close to use in pivot calculations. Allows forcing of a particular close irrespective of the data currently in use. For example, if ETH data is in use and this option is set to \"Force RTH\", it will use the H/L from the ETH session but the RTH close price. For futures, this honours the session and override times, if set and active", display = display.all - display.status_line, group = g_behaviourOptions)
float switchingTolerance = input.float (2, "ETH switching tolerance %", 0, 100, step = 0.1, tooltip = "Controls the tolerance before switching from RTH to ETH data in automatic mode. It represents the percentage of daily ATR that the price must exceed the RTH high/low by so that ETH data will be switched to. Useful for not being affected by minor differences between H/L data on an intra-day timeframe versus the daily timeframe", display = display.all - display.status_line, group = g_behaviourOptions)
float pivotRangeThreshold = input.float(2, "Neutral range threshold %", 0, 100, step = 0.1, tooltip = "If the difference of yesterday's and 2-days ago close is within this percentage of the daily ATR, the range is considered to be neutral", display = display.all - display.status_line, group = g_behaviourOptions)
float pivotWidthTheshold = input.float(2, "Similar pivot width threshold %", 0, 100, tooltip = "Specifies the threshold percentage which decides whether the pivots are considered similar width on the current day compared to the previous day. The percentage is of the difference between the S3-R3 ranges", display = display.all - display.status_line, group = g_behaviourOptions )

// Preconditions
bool validRangePrecondition = input.bool(true, "Range is valid or neutral", tooltip = "When enabled, we must be in a valid range for the play or in a neutral range. When disabled, plays from both ranges will be shown irrespective of the current range", display = display.all - display.status_line, group = g_playPreconditions)
bool noNeutralPlays = input.bool(true, "Not in neutral range", tooltip = "When in a neutral range, controls whether to show plays. When in a neutral range and this option is disabled, plays from both higher and lower ranges will be shown; when enabled, no plays will be shown", display = display.all - display.status_line, group = g_playPreconditions )
bool validWidthPrecondition = input.bool(true, "Width is valid or similar", tooltip = "When enabled, we must have a valid width for the play or have a similar width. When disabled, all plays will be allowed irrespective of current width", display = display.all - display.status_line, group = g_playPreconditions)
bool regionPrecondition = input.bool(true, "Region is correct", tooltip = "Ensures the prices is currently in the general region of the play entry", display = display.all - display.status_line, group = g_playPreconditions)
bool rthOpenPrecondition = input.bool(true, "Opened correctly", tooltip = "Ensures the price at the RTH open is in the correct location. No effect in premarket", display = display.all - display.status_line, group = g_playPreconditions)
bool rthPlays = input.bool(true, "In regular market hours", tooltip = "Control whether plays are shown only in regular market, or outside it too", display = display.all - display.status_line, group = g_playPreconditions)
bool blockingPrecondition = input.bool(true, "No bad price action", tooltip = "If after the open the price goes too far in the wrong direction for a specific play, that play will be blocked for the rest of the day", display = display.all - display.status_line, group = g_playPreconditions)

// Futures options
string timezoneIn = input.string("Exchange", "Timezone for sessions", options = ["Exchange", "Pacific/Honolulu", "America/Anchorage", "America/Los_Angeles", "America/Denver", "America/Chicago", "America/New_York", "America/Halifax", "America/Argentina/Buenos_Aires", "Europe/London", "Europe/Berlin", "Europe/Moscow", "Asia/Dubai", "Asia/Karachi", "Asia/Kolkata", "Asia/Bangkok", "Asia/Shanghai", "Asia/Tokyo", "Australia/Brisbane", "Australia/Sydney", "Pacific/Auckland"], tooltip = "Set the timezone that the times in the sessions below are specifed in. \"Exchange\" uses the timezone of the exchange. Choose a location in the same timezone as you if want to specify times in your local timezone. They are ordered from largest negative offset from UTC to largest positive offset. Also affects the time shown in alert messages", display = display.all - display.status_line, group = g_futuresOptions )
string futureRegSess = input.session("0830-1500", "Futures regular session", tooltip = "Specifies pseudo-RTH session times for futures in the exchange timezone (ES/NQ in on Central Time which is why default is 08:30-15:00 with the exchange timezone). This is where the most volume is. For example, despite ES and NQ on CME being traded 23 hours per day during the week (17:00 till 16:00 the next day, Sunday afternoon to Friday afternoon, Central Time), most of the trading activity happens between 08:30 and 15:00 Central Time. Before this (from 17:00 the previous day) we call it pseudo-premarket, after this (until 16:00) we call is pseudo-postmarket", display = display.all - display.status_line, group = g_futuresOptions)

 // Developer options
bool enableDebug = DEV_MODE ? input.bool(false, "Debug mode", display = display.all - display.status_line, group = g_developerOptions) : false
float debugBoolPrice = DEV_MODE ? input.float(100, "Debug bool price", display = display.all - display.status_line, group = g_developerOptions) : na
string debugCategory = DEV_MODE ? input.string("", "Debug category", group = g_developerOptions) : na

//#endregion ---- Input

//#region ---- Functions

//@function             Utility function for debug logging.
//@param logPredicate   Predicate to allow control on which bar the debug function creates output. Setting to true will produce a message on every bar.
//@param message        The string to log
//@returns              Nothing
//Dependancy            Global bool enableDebug controlling whether this produces any log output
debug(bool logPredicate, string category, string message) =>
    if enableDebug and logPredicate and debugCategory == category
        log.info(message)        

//@function             Utility function to be used with the plot() function and useful for debugging.
//@param category       A debug category to allow filtering of debug calls.
//@returns              A level to be used in the plot() function
debugPlot(float level, string category) =>
    enableDebug and debugCategory == category ? level : na


//@function             Similar to debugPlot, but can't use a global variable in an overloaded function in Pine script so had to make a seperate function
//@param _bool          A bool to plot. True values will be plotted at the debugBoolPrice level
//@returns              A level to be used in the plot() function
debugPlotBool(bool _bool, string category) =>
    debugPlot(_bool ? debugBoolPrice : 0, category)


//@function             Function to get the number of minutes in the extended session for some common exchanges (defaulting to 23 hour market otherwise). There is no other way to get this info, given we need to assign to a "simple" variable
//@param tfMin          Time frame in minutes
//@returns              Number of bars
getSessionBarCount(simple int tfMin) =>
    simple string prefix = syminfo.prefix
    simple int sessionMinutes =
         syminfo.type == "index" ? 390 : // Indices qouted during regualr hours, like SPX: 08:30–15:00 CT. Non-US indices will probably be close, though you may see one day more or less than set in the option
         prefix == "NASDAQ" or prefix == "NASDAQ_DLY" or prefix == "NYSE" or prefix == "NYSE_DLY" or prefix == "AMEX" or prefix == "AMEX_DLY" or prefix == "BATS" or prefix == "BATS_DLY" or prefix == "ARCA" or prefix == "ARCA_DLY" or prefix == "IEX" or prefix == "IEX_DLY" or prefix == "EDGE" or prefix == "EDGE_DLY" ? 960 :  // US stocks: 04:00–20:00 ET extended         
         prefix == "CME" or prefix == "CME_DL" or prefix == "CME_MINI" or prefix == "CME_MINI_DL" or prefix == "CBOT" or prefix == "CBOT_DL" or prefix == "CBOT_MINI" or prefix == "CBOT_MINI_DL" or prefix == "NYMEX" or prefix == "NYMEX_DL" or prefix == "NYMEX_MINI" or prefix == "NYMEX__MINI_DL" or prefix == "COMEX" or prefix == "COMEX_DL" or prefix == "COMEX_MINI" or prefix == "COMEX_MINI_DL" ? 1380 :  // US futures: 17:00–16:00 CT with 1-hour break
         prefix == "EUREX" or prefix == "EUREX_DLY" ? 840 :  // EUREX futures: approx 01:10–15:50 UTC
         prefix == "EURONEXT" or prefix == "EURONEXT_DLY" ? 510 :  // Euronext: 09:00–17:30 CET
         prefix == "XETR" or prefix == "XETR_DLY" or prefix == "XTRA" or prefix == "XTRA_DLY" or prefix == "FWB" or prefix == "FWB_DLY" or prefix == "GETTEX" or prefix == "GETTEX_DLY" or prefix == "TRADEGATE" or prefix == "TRADEGATE_DLY" ? 510 :  // Germany: 09:00–17:30 CET
         prefix == "LSE" or prefix == "LSE_DLY" or prefix == "LSIN" or prefix == "LSIN_DLY" ? 510 :  // UK: 08:00–16:30 local time
         prefix == "BME" or prefix == "BME_DLY" or prefix == "MC" or prefix == "MC_DLY" ? 510 :  // Spain: 09:00–17:30 CET
         prefix == "MIL" or prefix == "MIL_DLY" or prefix == "BIT" or prefix == "BIT_DLY" ? 510 :  // Italy: 09:00–17:30 CET
         prefix == "SIX" or prefix == "SIX_DLY" or prefix == "VTX" or prefix == "VTX_DLY" ? 510 :  // Switzerland: 09:00–17:30 CET
         prefix == "NSE" or prefix == "NSE_DLY" or prefix == "NSE_IND" or prefix == "BSE" or prefix == "BSE_DLY" ? 377 :  // India: 09:15–15:36 IST + 2 candles
         prefix == "TSE" or prefix == "TSE_DLY" or prefix == "JPX" or prefix == "JPX_DLY" ? 375 :  // Japan: 09:00–15:00 with 1-hour lunch break
         prefix == "HKEX" or prefix == "HKEX_DLY" or prefix == "SEHK" or prefix == "SEHK_DLY" ? 330 :  // Hong Kong: 09:30–12:00, 13:00–16:00
         prefix == "KRX" or prefix == "KRX_DLY" ? 360 :  // Korea: 09:00–15:30
         prefix == "SGX" or prefix == "SGX_DLY" ? 450 :  // Singapore: 09:00–17:00 with 1-hour lunch break
         prefix == "ASX" or prefix == "ASX_DLY" ? 360 :  // Australia: 10:00–16:00 local
         prefix == "TSX" or prefix == "TSX_DLY" or prefix == "TSXV" or prefix == "TSXV_DLY" ? 390 :  // Canada: 09:30–16:00 ET         
         prefix == "AMS" or prefix == "AMS_DLY" ? 510 :  // Netherlands (Euronext Amsterdam): 09:00–17:30 CET
         prefix == "VIE" or prefix == "VIE_DLY" or prefix == "XWBO" or prefix == "XWBO_DLY" ? 510 :  // Austria (Vienna Stock Exchange): 09:00–17:30 CET
         prefix == "WSE" or prefix == "WSE_DLY" or prefix == "GPW" or prefix == "GPW_DLY" ? 470 :  // Poland (Warsaw Stock Exchange): 09:00–16:50 CET
         1380  // fallback to 23 hour market
    math.floor(sessionMinutes / tfMin)

//@function     Calculate Camarilla pivot points. Only calculates 3, 4 and 6 since these are the only relevant ones in Thor's system
//@param h      Daily high
//@param l      Daily Low
//@param c      Daily Close
//@returns      A map of the cams
calculateCams(float h, float l, float c) =>            
    map<string,float> camMap = map.new<string,float>()    
    camMap.put("R2", c + (h - l) * 1.1 / 6)
    camMap.put("R3", c + (h - l) * 1.1 / 4)
    camMap.put("R4", c + (h - l) * 1.1 / 2)    
    camMap.put("R6", c * h / l)    
    camMap.put("S2", c - (h - l) * 1.1 / 6)
    camMap.put("S3", c - (h - l) * 1.1 / 4)
    camMap.put("S4", c - (h - l) * 1.1 / 2)    
    camMap.put("S6", c - (camMap.get("R6") - c))
    camMap

//@function             Compares 2 closing values to find whether the first is greater than the second within some tolerance determined by a percentage of a base close
//@param primaryClose   The compared which should be higher for the function to return true
//@param SecondaryClose The comparing value
//@param comparisonBase The value on which to apply a threshold percentage.
//@param threshold      A percentage of the baseClose which specifes the tolerance
//@returns              A bool indicating the result of the comparison
compareClose(float primaryClose, float secondaryClose, float comparisonBase, float threshold) =>
    primaryClose - secondaryClose > comparisonBase * threshold / 100

//@function             Get the offset highest value in a series since the bar where the sinceCondition was true
//@param sinceCondition Boolean which controls how far back to look. It goes back until it reaches a bar where the condition is true
//@param offset         Gets the value for the bar "offset" bars prior to the found one (the bar or interest)
//@returns              The highest high on the bar of interest
getHighestSince(bool sinceCondition, int offset) =>            
    ta.highest(high, nz(ta.barssince(sinceCondition)) + 1)[offset]
    
//@function             Get the offset lowest value in a series since the bar where the sinceCondition was true
//@param sinceCondition Boolean which controls how far back to look. If goes back until it reaches a bar where the condition is true
//@param offset         Gets the value for the bar "offset" bars prior to the found one (the bar of interest)
//@returns              The lowest low on the bar of interest
getLowestSince(bool sinceCondition, int offset) =>        
    ta.lowest(low, nz(ta.barssince(sinceCondition)) + 1)[offset]

//@function                 Gets remembered closes, highs and lows, but only updates its memory on a condition
//@param HLCondition        Predicate specifying how far back to search for high/low
//@param updateCondition    Condition specifying when to update memory
//@param shiftBackCondition Condition which if true causes the returned values to be shifted back by one remembered value (i.e. the remembered values before the last update)
//@returns                  A tuple of the desired values
f_pastData(bool HLCondition, bool updateCondition, bool shiftBackCondition) =>
    var float close0 = na
    var float close1 = na
    var float close2 = na
    var float high0 = na
    var float low0 = na
    var float high1 = na
    var float low1 = na
    lastHigh = getHighestSince(HLCondition, 1)
    lastLow = getLowestSince(HLCondition, 1)
    if updateCondition
        close2 := close1
        close1 := close0
        close0 := close[1]
        high1 := high0        
        high0 := lastHigh        
        low1 := low0
        low0 := lastLow
    if shiftBackCondition
        [close1, close2, high1, low1]
    else
        [close0, close1, high0, low0]    

//@function                 Gets remembered RTH open and the premarket high and low for the current day, but only updates its memory on a condition
//@param HLCondition        Predicate specifying how far back to search for high/low
//@param updateCondition    Condition specifying when to update memory
//@param preOpenCondition   X    
//@returns                  A tuple of the desired values
f_currentData(bool HLCondition, bool updateCondition) =>
    var float usedOpen = na
    var float preHigh = na
    var float preLow = na
    lastHigh = getHighestSince(HLCondition, 0)
    lastLow = getLowestSince(HLCondition, 0)    
    if updateCondition        
        preHigh := lastHigh
        preLow := lastLow    
    [preHigh, preLow]

//@function                 Gets remembered high and low on the the previous bar since some HLCondition, but only updates its memory on a condition
//@param HLCondition        Predicate specifying how far back to search for high/low
//@param updateCondition    Condition specifying when to update memory
//@returns                  A tuple of the desired values
f_HLData(bool HLCondition, bool updateCondition) =>    
    var float remHigh = na
    var float remLow = na
    lastHigh = getHighestSince(HLCondition, 1)
    lastLow = getLowestSince(HLCondition, 1)
    if updateCondition        
        remHigh := lastHigh
        remLow := lastLow
    [remHigh, remLow]

//@function                 Get a previously remembered value and updates its memory on a condition
//@param value              The value on the previous bar to remember
//@param updatePredicate    Event on which to update memory
//@returns                  The remembered value
snapshotVal(float value, bool updatePredicate) =>
    var float snapshotValue = na    
    if updatePredicate
        snapshotValue := value[1]
    snapshotValue

//@function     Overloaded snapshotVal(float) for bools
snapshotVal(bool _bool, bool updatePredicate) =>
    _snapshotVal = snapshotVal(_bool == false ? 0.0 : 1.0, updatePredicate)
    _snapshotVal == 0.0 ? false : true

//@function         Function to give best text colour given background colour (taking into account transparency and the canvas, assuming lightMode is correctly set)
//@param bgcolour   The background colour of the element on which the text is drawn
//@returns          White or black, whichever is the best text colour
contrastTextColour(color bgColour) =>
    transparency = color.t(bgColour) / 100.0
    r = ((1 - transparency) * color.r(bgColour) + (lightMode ? transparency * 255 : 0)) / 255
    g = ((1 - transparency) * color.g(bgColour) + (lightMode ? transparency * 255 : 0)) / 255
    b = ((1 - transparency) * color.b(bgColour) + (lightMode ? transparency * 255 : 0)) / 255
    lum = 0.2126 * r + 0.7152 * g + 0.0722 * b
    lum > 0.5 ? color.rgb(0, 0, 0) : color.white
              
//@function     Parse a session string to the start and end hour and minute
parseSession(string session) =>
    // Assumes session is always in the format "HHMM-HHMM", e.g. "0930-1600"
    string startStr = str.substring(session, 0, 4)
    string endStr   = str.substring(session, 5, 9)

    int startHour   = int(str.tonumber(str.substring(startStr, 0, 2)))
    int startMinute = int(str.tonumber(str.substring(startStr, 2, 4)))
    int endHour     = int(str.tonumber(str.substring(endStr, 0, 2)))
    int endMinute   = int(str.tonumber(str.substring(endStr, 2, 4)))

    [startHour, startMinute, endHour, endMinute]

//#endregion ---- Functions

//#region --- Logic

// Get timezone in case "Exchange" is chosen in the options
string timezone = timezoneIn == "Exchange" ? syminfo.timezone : timezoneIn

// Markers for specific bars
bool regStartBar = false
bool preStartBar = false
bool postStartBar = false

bool isFutures = syminfo.type == "futures"

// Mark the first bars of regular hours, premarket and postmarket. This works irrespective of whether the chart displays RTH or ETH
// bar_index check is needed to force the value to true on the first bar or before a certain period of time (15000 bars) to prevent runtime error
if isFutures
    bool isInReg = not na(time(timeframe.period, futureRegSess, timezone)) 
    bool wasInReg = not na(time(timeframe.period, futureRegSess, timezone, 1))

    // Hacky way to determine if the user has set futures RTH to full time range (i.e. all bars are in RTH). Need to handle this specially since regStartBar and postStarBar would never trigger without it. 
    var bool allRth = true
    allRth := isInReg and allRth
    bool forceStart = false
    if allRth
        int startHour = int(str.tonumber(str.substring(futureRegSess, 0, 2)))
        int startMinute = int(str.tonumber(str.substring(futureRegSess, 2, 4)))
        int sessionStartTime = timestamp(timezone, year(time, timezone), month(time, timezone), dayofmonth(time, timezone), startHour, startMinute)
        forceStart := time == sessionStartTime

    regStartBar := isInReg and not wasInReg or forceStart or bar_index == 0
    preStartBar := session.isfirstbar or bar_index == 0
    postStartBar := wasInReg and not isInReg or forceStart or bar_index == 0
        
else     
    regStartBar := session.isfirstbar_regular or bar_index == 0 or bar_index <= last_bar_index - 15000
    preStartBar := session.ispremarket and session.isfirstbar or session.isfirstbar and session.isfirstbar_regular or bar_index == 0 or bar_index <= last_bar_index - 15000
    postStartBar := session.ispostmarket and session.ismarket[1] or session.isfirstbar and session.ismarket[1] or bar_index == 0 or bar_index <= last_bar_index - 15000    

// plot(debugPlotBool(preStartBar, "startbars"), color = color.fuchsia, display = display.all - display.status_line)

bool isPremarket = if isFutures
    ta.barssince(preStartBar) < ta.barssince(regStartBar)
else
    session.ispremarket    
    
bool isPostmarket = if isFutures
    bool firstCondition = ta.barssince(postStartBar) < ta.barssince(preStartBar)
    bool secondCondition = ta.barssince(regStartBar) < ta.barssince(preStartBar)
    bool thirdCondition = ta.barssince(postStartBar) < ta.barssince(regStartBar)
    firstCondition and secondCondition and thirdCondition
else 
    session.ispostmarket    

bool isMarket = not isPremarket and not isPostmarket

// Calculates bars from days
simple int numBars = (numDays + 2) * getSessionBarCount(math.min(timeframe.multiplier, 30))
// Prevents runtime errors on non-supported security types or timeframe (allowing the managed runtime errors or warnings to trigger)
numBars := na(numBars) or numBars == 0 ? 1 : numBars

// Basically cripple the intra-day indicator on D/W/M timeframe. Avoids a runtime error.
if timeframe.isdwm or timeframe.isseconds
    numBars := 1
    regStartBar := true
    preStartBar := true
    postStartBar := true

// Get ETH-enabled version of current symbol
simple string ethTicker = ticker.modify(syminfo.main_tickerid, session.extended, adjustment.none, backadjustment.on)

// Get all the numbers we need
[yRthClose, yyRthClose, yRthHigh, yRthLow] = request.security(ethTicker, str.tostring(math.min(timeframe.multiplier, 30)), f_pastData(regStartBar, postStartBar, isPostmarket), calc_bars_count = numBars)
[yEthClose, yyEthClose, yEthHigh, yEthLow] = request.security(ethTicker, str.tostring(math.min(timeframe.multiplier, 30)), f_pastData(preStartBar, preStartBar, false), calc_bars_count = numBars)
[preHigh, preLow] = request.security(ethTicker, str.tostring(math.min(timeframe.multiplier, 30)), f_currentData(preStartBar, isPremarket), calc_bars_count = numBars)
[yPostHigh, yPostLow] = request.security(ethTicker, str.tostring(math.min(timeframe.multiplier, 30)), f_HLData(postStartBar, preStartBar), calc_bars_count = numBars)

var float rthOpen = na
rthOpen := regStartBar ? open : rthOpen

float dailyATR = request.security(syminfo.main_tickerid, "D", ta.atr(14)[1], lookahead = barmerge.lookahead_on)

// Set calculation mode
bool useEthForCams = switch calcMode
    CalcMode.auto =>
        math.max(yRthHigh, yPostHigh, preHigh) - yRthHigh > dailyATR * switchingTolerance / 100
     or yRthLow - math.min(yRthLow,  yPostLow, preLow) > dailyATR * switchingTolerance / 100     
    CalcMode.forceETH => true
    CalcMode.forceRTH => false

// Get the Central Pivots
float CP = na

switch
    useClose == UseClose.rth => CP := yRthClose
    useClose == UseClose.eth => CP := yEthClose
    useClose == UseClose.auto and not useEthForCams => CP := yRthClose
    useClose == UseClose.auto and useEthForCams => CP := yEthClose
    => runtime.error("Invalid CP/oCP")

bool yUseEthForCams = snapshotVal(useEthForCams, preStartBar)
float yCP = useClose == UseClose.eth or useClose == UseClose.auto and yUseEthForCams ? yyEthClose : yyRthClose

debug(barstate.islastconfirmedhistory, "yuseeth", str.tostring(yUseEthForCams))

// Determine Cam range
PivotRange pivotRange = switch        
    compareClose(CP, yCP, dailyATR, pivotRangeThreshold) => PivotRange.higher
    compareClose(yCP, CP, dailyATR, pivotRangeThreshold) => PivotRange.lower
    => PivotRange.neutral

debug(barstate.islastconfirmedhistory, "range", str.tostring(CP) + " " + str.tostring(yCP))

// Calculate Cams
var map<string,float> camMap = na
camMap := if useEthForCams
    calculateCams(yEthHigh, yEthLow, CP)
else
    calculateCams(yRthHigh, yRthLow, CP)

// Calculate pivot width
float yS3 = snapshotVal(camMap.get("S3"), preStartBar)
float yR3 = snapshotVal(camMap.get("R3"), preStartBar)
PivotWidth pivotWidth = switch
    camMap.get("R3") - camMap.get("S3") > (yR3 - yS3) * (1 + pivotWidthTheshold / 100) => PivotWidth.wide
    camMap.get("R3") - camMap.get("S3") < (yR3 - yS3) * (1 - pivotWidthTheshold / 100) => PivotWidth.narrow
    => PivotWidth.similar

debug(barstate.islastconfirmedhistory, "pivotwidth", "R3: " + str.tostring(camMap.get("R3")) + ", S3: " + str.tostring(camMap.get("S3")))
debug(barstate.islastconfirmedhistory, "pivotwidth", "yR3: " + str.tostring(yR3) + ", yS3: " + str.tostring(yS3))
debug(barstate.islastconfirmedhistory, "pivotwidth", "diff: " + str.tostring(camMap.get("R3") - camMap.get("S3")) + ", ydiff: " + str.tostring((yR3 - yS3)))

//#region ---- Plays
 
// For convenience in the play specifications
float R2 = camMap.get("R2")
float R3 = camMap.get("R3")
float R4 = camMap.get("R4")
float R6 = camMap.get("R6")
float S2 = camMap.get("S2")
float S3 = camMap.get("S3")
float S4 = camMap.get("S4")
float S6 = camMap.get("S6")

PlaySpec playSpec = na
PlayState playState = na

// Initialise playStateMap on first bar
if barstate.isfirst
    for play in PLAYS
        playStateMap.put(play, PlayState.new())   

//#region ---- Play specifications

// Higher Range, Play A
playSpecMap.put("HA", PlaySpec.new(
  description = "S3 to R3 traversal",
  direction = Direction.long,
  validRange = PivotRange.higher,
  validWidth = PivotWidth.wide,
  entry_precondition_region = close > S4 and close < CP,
  entry_precondition_rthOpen = rthOpen > S2 and rthOpen < R2,    
  block_play = high > R3
  ))

// Higher Range, Play B
playSpecMap.put("HB", PlaySpec.new(
  description = "Trending R4 break up",
  direction = Direction.long,
  validRange = PivotRange.higher,
  validWidth = PivotWidth.narrow,
  entry_precondition_region = close > R3 and close < R6,
  entry_precondition_rthOpen = rthOpen > S2 and rthOpen < R2,
  block_play = low < S3
  ))

// Higher Range, Play C
playSpecMap.put("HC", PlaySpec.new(
  description = "Trending R4 break up",
  direction = Direction.long,
  validRange = PivotRange.higher,
  validWidth = PivotWidth.narrow,
  entry_precondition_region = close > R3,
  entry_precondition_rthOpen = rthOpen > R4 and rthOpen < R6,
  block_play = high > R6
  ))

// Higher Range, Play D
playSpecMap.put("HD", PlaySpec.new(
  description = "Counter-trending S4 break down",
  direction = Direction.short,
  validRange = PivotRange.higher,  
  validWidth = PivotWidth.narrow,
  entry_precondition_region = close < S3,
  entry_precondition_rthOpen = rthOpen < S4 and rthOpen > S6,
  block_play = low < S6
  ))

// Higher Range, Play E
playSpecMap.put("HE", PlaySpec.new(
  description = "R4 extreme reversal",
  direction = Direction.short,
  validRange = PivotRange.higher,
  validWidth = PivotWidth.wide,
  entry_precondition_region = close > R3 and close < R6, 
  entry_precondition_rthOpen = rthOpen > S2 and rthOpen < R2,
  block_play = low < S3
  ))

// Higher Range, Play F
playSpecMap.put("HF", PlaySpec.new(
  description = "R6 reversal",
  direction = Direction.short,
  validRange = PivotRange.higher,
  validWidth = PivotWidth.versatile,
  entry_precondition_region = close > R4,
  entry_precondition_rthOpen = rthOpen > R4 and rthOpen < R6,
  block_play = false
  ))

// Lower Range, Play A
playSpecMap.put("LA", PlaySpec.new(
  description = "R3 to S3 traversal",
  direction = Direction.short,
  validRange = PivotRange.lower,
  validWidth = PivotWidth.wide,
  entry_precondition_region = close < R4 and close > CP,
  entry_precondition_rthOpen = rthOpen > S2 and rthOpen < R2,
  block_play = low < S3  
  ))

// Lower Range, Play B
playSpecMap.put("LB", PlaySpec.new(
  description = "Trending S4 break down",
  direction = Direction.short,
  validRange = PivotRange.lower,
  validWidth = PivotWidth.narrow,
  entry_precondition_region = close < S3 and close > S6,
  entry_precondition_rthOpen = rthOpen > S2 and rthOpen < R2,
  block_play = high > R3  
  ))

// Lower Range, Play C
playSpecMap.put("LC", PlaySpec.new(
  description = "Trending S4 break down",
  direction = Direction.short,
  validRange = PivotRange.lower,
  validWidth = PivotWidth.narrow,
  entry_precondition_region = close < S3, 
  entry_precondition_rthOpen = rthOpen < S4 and rthOpen > S6,
  block_play = low < S6
  ))

// Lower Range, Play D
playSpecMap.put("LD", PlaySpec.new(
  description = "Counter-trending R4 break up",
  direction = Direction.long,
  validRange = PivotRange.lower,
  validWidth = PivotWidth.narrow,
  entry_precondition_region = close > R3,
  entry_precondition_rthOpen = rthOpen > R4 and rthOpen < R6,
  block_play = high > R6  
  ))

// Lower Range, Play E
playSpecMap.put("LE", PlaySpec.new(
  description = "S4 extreme reversal",
  direction = Direction.long,
  validRange = PivotRange.lower,
  validWidth = PivotWidth.wide,
  entry_precondition_region = close < S3 and close > S6,
  entry_precondition_rthOpen = rthOpen > S2 and rthOpen < R2,
  block_play = high > R3  
  ))

// Lower Range, Play F
playSpecMap.put("LF", PlaySpec.new(
  description = "S6 reversal",
  direction = Direction.long,
  validRange = PivotRange.lower,
  validWidth = PivotWidth.versatile,
  entry_precondition_region = close < S4,
  entry_precondition_rthOpen = rthOpen < S4 and rthOpen > S6,
  block_play = false  
  ))

//#endregion ---- Play specifications

// Process the plays
for play in PLAYS
    
    playSpec := playSpecMap.get(play)
    playState := playStateMap.get(play)    

    // Unblock plays at ETH open and RTH open
    if preStartBar or regStartBar
        playState.play_blocked := false
        
    bool validRangePC = pivotRange == playSpec.validRange or not validRangePrecondition or pivotRange == PivotRange.neutral    
    bool neutralPlaysPC = pivotRange != PivotRange.neutral or not noNeutralPlays    
    bool validWidthPC = not validWidthPrecondition or pivotWidth == playSpec.validWidth or playSpec.validWidth == PivotWidth.versatile or pivotWidth == PivotWidth.similar
    bool validRegionPC = not regionPrecondition or playSpec.entry_precondition_region
    bool rthOpenPC = not rthOpenPrecondition or isPremarket or playSpec.entry_precondition_rthOpen    
    bool rthPlaysPC = not rthPlays or isMarket
    bool blockingPC = not blockingPrecondition or (not playState.play_blocked and not playSpec.block_play)
    
    bool preconditions = validRegionPC and validWidthPC and rthPlaysPC and validRangePC and neutralPlaysPC and rthOpenPC and blockingPC

    playState.playPossible := preconditions            
    playState.play_blocked := playSpec.block_play or playState.play_blocked

    playStateMap.put(play, playState)


//#endregion ---- Plays
//#endregion ---- Logic

//#region ---- Visuals

// The cams plotted on the first day are incorrect since if need data from the previous day with in not retrieved. So hide the first day.
var bool firstCP = false
var bool safePlot = false
firstCP := na(CP[1]) and not na(CP) or firstCP
safePlot := firstCP and CP != CP[1] and timeframe.change("D") or safePlot

bool lineBreak = not lineJoin and preStartBar

// Plot the Cams
plot(safePlot and showCP ? CP : na, "CP", color = lineBreak ? color.new(cpColour, 100) : cpColour, linewidth = 1, display = display.all - display.status_line)
cpr_l = plot(safePlot and showCPR ? S2 : na, "S2", color = lineBreak ? color.new(cprColour, 100) : cprColour, linewidth = 1, display = display.all - display.status_line)
cpr_h = plot(safePlot and showCPR ? R2 : na, "R2", color = lineBreak ? color.new(cprColour, 100) : cprColour, linewidth = 1, display = display.all - display.status_line)
fill(cpr_l, cpr_h, color = lineBreak ? color.new(cprColour, 100) : cprColour)

plot(safePlot ? R3 : na, "R3", lineBreak ? color.new(resistanceColour, 100) : resistanceColour, 3, display = display.all - display.status_line)
plot(safePlot ? R4 : na, "R4", lineBreak ? color.new(resistanceColour, 100) : resistanceColour, 2, display = display.all - display.status_line)
plot(safePlot ? R6 : na, "R6", lineBreak ? color.new(resistanceColour, 100) : resistanceColour, 1, display = display.all - display.status_line)
plot(safePlot ? S3 : na, "S3", lineBreak ? color.new(supportColour, 100) : supportColour, 3, display = display.all - display.status_line)
plot(safePlot ? S4 : na, "S4", lineBreak ? color.new(supportColour, 100) : supportColour, 2, display = display.all - display.status_line)
plot(safePlot ? S6 : na, "S6", lineBreak ? color.new(supportColour, 100) : supportColour, 1, display = display.all - display.status_line)

//#region ---- Extension lines and labels

// Extention line handles
var map<string, ExtLine> extLineMap = map.new<string, ExtLine>()
var map<string, ExtLine> oExtLineMap = map.new<string, ExtLine>()
var map<string, ExtLine> wExtLineMap = map.new<string, ExtLine>()
var map<string, ExtLine> mExtLineMap = map.new<string, ExtLine>()

if barstate.islast and not timeframe.isdwm and not timeframe.isseconds

    for cam in CAM_LEVELS

        // Check whether the extension line/label have already been created.
        if na(extLineMap.get(cam))

            // Initialise the extension lines            
            ExtLine extLine = ExtLine.new()
            color colour = switch str.substring(cam, 0, 1)
                "R" => resistanceColour
                "S" => supportColour
                => color.white
            int linewidth = 4 - int(str.tonumber(str.substring(cam, 1)) / 2)
            extLine.h_line := line.new(bar_index, camMap.get(cam), bar_index + labelOffset, camMap.get(cam), extend = extendLines ? extend.right : extend.none, color = colour, style = line.style_solid, width = linewidth)
            extLine.h_label := label.new(bar_index + labelOffset, camMap.get(cam), cam + (showPrices ? ": " + str.tostring(camMap.get(cam), "#.00") : na), color = colour, style = label.style_label_left, textcolor = contrastTextColour(colour))
            extLineMap.put(cam, extLine)

        // Update the extension lines and labels on the last bar
        line h_line = extLineMap.get(cam).h_line
        label h_label = extLineMap.get(cam).h_label
        line.set_xy1(h_line, bar_index, camMap.get(cam))
        line.set_xy2(h_line, bar_index + labelOffset, camMap.get(cam))
        label.set_xy(h_label, bar_index + labelOffset, camMap.get(cam))
        label.set_text(h_label, cam  + (showPrices ? ": " + str.tostring(camMap.get(cam), "#.00") : na))

    // Show Central Pivots
    if showCP        
        var line lineClose = line.new(bar_index, CP, bar_index + labelOffset, CP, extend = extendLines ? extend.right : extend.none, color = cpColour, style = line.style_solid, width = 1)    
        var label labelClose = label.new(bar_index + labelOffset, CP, "CP" + (showPrices ? ": " + str.tostring(CP, "#.00") : na), color = cpColour, style = label.style_label_left, textcolor = contrastTextColour(cpColour)) 
        line.set_xy1(lineClose, bar_index, CP)
        line.set_xy2(lineClose, bar_index + labelOffset, CP)
        label.set_xy(labelClose, bar_index + labelOffset, CP)
        label.set_text(labelClose, "CP" + (showPrices ? ": " + str.tostring(CP, "#.00") : na))


//#endregion ---- Extension lines and labels
 
//#region ---- Table

string playText = na

for play in PLAYS
    playText := playText + (playStateMap.get(play).playPossible ? play + ", " : na)

if not userConf
    playText := "Please read the indicator description\nand confirm you have done this in the\nsettings to enable full functionality" 
else
    if na(playText)
        playText := "No Candidate Plays" 
    else 
        playText := "Candidate Plays: " + str.substring(playText, 0, str.length(playText) - 2)

// Top right info table. (Re)Draw the table on the last bar
if barstate.islast
    var table infoTable = table.new(position.top_right, 3, 2, color.black, color.white, 1, color.white, 1)
    table.merge_cells(infoTable, 0, 1, 2, 1)
    if not timeframe.isdwm and not timeframe.isseconds
        table.cell(infoTable, 0, 0, useEthForCams ? "Using ETH Data" : "Using RTH Data", text_color = color.white)
        table.cell(infoTable, 1, 0, str.tostring(pivotRange), text_color = color.white)
        table.cell(infoTable, 2, 0, str.tostring(pivotWidth), text_color = color.white)
        table.cell(infoTable, 0, 1, playText, text_color = color.white)        
    else
        table.cell(infoTable, 0, 0, "───────", text_color = color.white)
        table.cell(infoTable, 1, 0, "───────", text_color = color.white)
        table.cell(infoTable, 2, 0, "───────", text_color = color.white)
        table.cell(infoTable, 0, 1, "Deactivated on D/W/M and secs timeframes", text_color = color.white)

//#endregion ---- Table
//#endregion ---- Visuals

//#region ---- Alerts

//#endregion ---- Alerts